﻿using Bullet;
using Cysharp.Threading.Tasks;
using Data;
using DG.Tweening;
using Hero;
using Infrastructure.States;
using Level;
using Level.Decorations;
using Services.Input;
using Services.Loot;
using Services.Progress;
using Services.StaticData;
using System;
using System.Collections.Generic;
using UI;
using UnityEngine;
using Object = UnityEngine.Object;

namespace Factory
{
    public class GameFactory : IGameFactory
    {
        public static event Action FloorGenerated;
        
        private readonly IProgressService _progressService;
        private readonly ILootService _lootService;
        private readonly IStaticDataService _staticDataService;
        private readonly IInputService _inputService;
        private readonly GameStateMachine _stateMachine;
        private GameSceneData _data;
        private HeroState _state;
        private HudStaticData _hudData;
        private LevelStaticData _levelData;

        public GameFactory(IProgressService progressService,
            ILootService lootService,
            IStaticDataService staticDataService,
            IInputService inputService, GameStateMachine stateMachine)
        {
            _progressService = progressService;
            _lootService = lootService;
            _staticDataService = staticDataService;
            _inputService = inputService;
            _stateMachine = stateMachine;
        }

        public async UniTask CreateGameField()
        {
            _data = _progressService.Progress.GameSceneData;
            _data.FloorElements = await CreateFloor(
                _data.FieldSideSize,
                _data.LevelGenerationTime,
                _data.CreateAnimationTime,
                _data.FloorElementPositionOffset);
            await UniTask.Delay(TimeSpan.FromSeconds(_data.CreateAnimationTime));
            FloorGenerated?.Invoke();
            List<Vector3> obstaclesPositions;
            (_data.Obstacles, obstaclesPositions) = await CreateObstacles(
                _data.FloorElements,
                _data.LevelGenerationTime,
                _data.CreateAnimationTime,
                _data.ObstaclesQuantityDivideFactor,
                _data.ObstacleTargetScale);
            _data.HeroSpawnPoint = GetStartPosition(
                _data.FloorElements,
                obstaclesPositions);
        }

        public (HeroHealth Health, HeroReload Reload, HeroScore Score, HeroShoot Shoot) CreateHero(Vector3 at)
        {
            _state = _progressService.Progress.HeroState;
            var heroData = _staticDataService.Hero;
            var hero = Object.Instantiate(heroData.Prefab, at, Quaternion.identity);
            _data.Hero = hero;
            hero.TryGetComponent<HeroHealth>(out var health);
            health.Construct(_data, _state);
            hero.TryGetComponent<HeroMove>(out var move);
            move.Speed = heroData.MoveSpeed;
            move.Construct(_inputService);
            hero.TryGetComponent<HeroRotate>(out var rotate);
            rotate.Speed = heroData.RotateSpeed;
            rotate.Construct(_inputService);
            hero.TryGetComponent<HeroReload>(out var reload);
            reload.Delay = heroData.ReloadDelay;
            reload.Construct(_inputService, _state);
            hero.TryGetComponent<HeroShoot>(out var shoot);
            shoot.Delay = heroData.ShootDelay;
            shoot.Construct(_inputService, this, _state);
            hero.TryGetComponent<HeroScore>(out var score);
            score.Construct(_data, _state);
            hero.TryGetComponent<HeroDeath>(out var death);
            death.Construct(_stateMachine);
            
            return (health, reload, score, shoot);
        }

        public GameObject CreateHud(HeroHealth health, HeroReload reload, HeroScore score, HeroShoot shoot)
        {
            _hudData = _staticDataService.Hud;
            var obj = Object.Instantiate(_hudData.HudPrefab);
            _data.Hud = obj;
            obj.TryGetComponent<Hud>(out var hud);
            hud.Construct(
                this,
                _progressService,
                health,
                shoot,
                reload,
                score);
            var (lives, ammo) = hud.GetHolders();
            var livesList = new List<GameObject>();
            for (var i = 0; i < _state.CurrentLives; i++)
                livesList.Add(CreateHudLife(lives));
            var ammoList = new List<GameObject>();
            for (var i = 0; i < _state.CurrentAmmo; i++)
                ammoList.Add(CreateHudAmmo(ammo));
            hud.InitLivesList(livesList);
            hud.InitAmmoList(ammoList);
            hud.SetScore();
            
            return obj;
        }

        public GameObject CreateHudLife(Transform where) =>
            Object.Instantiate(_hudData.LifePrefab, where);

        public GameObject CreateHudAmmo(Transform where) =>
            Object.Instantiate(_hudData.AmmoPrefab, where);

        public GameObject CreateBullet(Vector3 at, Quaternion direction)
        {
            var bulletData = _staticDataService.Bullet;
            var bullet = Object.Instantiate(bulletData.Prefab, at, direction);
            bullet.TryGetComponent<BulletMove>(out var move);
            move.Speed = bulletData.Speed;
            move.LifeTime = bulletData.LifeTime;
            bullet.TryGetComponent<BulletDamage>(out var damage);
            damage.Damage = bulletData.Damage;
            _data.Bullets.Add(bullet);
            return bullet;
        }

        private async UniTask<List<FloorElement>> CreateFloor(
            int size, 
            float generationTime, 
            float scaleAnimationTime,
            float offset)
        {
            var floor = new List<FloorElement>();

            var creationDelay = generationTime / (size * size);

            var x = -offset;
            var y = -offset;

            var dirIndex = 0;
            var steps = 1;
            var stepsMade = 0;
            var turns = 0;

            var directions = new Vector2[]
            {
                new(1, 0),
                new(0, 1),
                new(-1, 0),
                new(0, -1)
            };

            for (var i = 0; i < size * size; i++)
            {
                var position = new Vector3(x, 0, y);
                var obj = CreateFloorElement(position);
                obj.TryGetComponent<FloorElement>(out var element);
                element.Id = i;
                element.Position = position;
                floor.Add(element);

                element.Visualize(scaleAnimationTime);
                await UniTask.Delay(TimeSpan.FromSeconds(creationDelay));

                x += directions[dirIndex].x;
                y += directions[dirIndex].y;
                stepsMade++;

                if (stepsMade != steps)
                    continue;

                dirIndex = (dirIndex + 1) % 4;
                stepsMade = 0;
                turns++;

                if (turns % 2 == 0)
                    steps++;

            }

            return floor;
        }

        private GameObject CreateFloorElement(Vector3 at)
        {
            _levelData = _staticDataService.Level;
            return Object.Instantiate(_levelData.FloorPrefab, at, Quaternion.identity);
        }

        private async UniTask<(List<Obstacle>, List<Vector3>)> CreateObstacles(List<FloorElement> floorElements,
            float generationTime, float scaleAnimationTime, int divideFactor, float targetScale)
        {
            var animationTime = generationTime / floorElements.Count;
            var quantity = floorElements.Count / divideFactor;
            var obstacles = new List<Obstacle>(quantity);

            var positions = GetObstaclesPositions(floorElements, quantity);

            foreach (var position in positions)
            {
                var obj = CreateObstacle(position);
                obj.TryGetComponent<Obstacle>(out var obstacle);
                obstacle.Position = position;
                obstacles.Add(obstacle);

                obstacle.transform.DOScaleY(targetScale, scaleAnimationTime).SetEase(Ease.OutBounce);
                await UniTask.Delay(TimeSpan.FromSeconds(animationTime));
            }

            return (obstacles, positions);
        }

        private List<Vector3> GetObstaclesPositions(List<FloorElement> floorElements, int quantity)
        {
            var positions = new List<Vector3>();
            var indexList = new List<int>();

            for (var i = 0; i < quantity; i++)
            {
                var newObstacleIndex = Utils.GetRandomIndex(floorElements.Count);
                while (indexList.Contains(newObstacleIndex))
                    newObstacleIndex = Utils.GetRandomIndex(floorElements.Count);

                indexList.Add(newObstacleIndex);
                positions.Add(floorElements[newObstacleIndex].Position);
            }

            return positions;
        }

        private Vector3 GetStartPosition(List<FloorElement> floorElements, List<Vector3> obstaclesPositions)
        {
            var i = 0;
            var firstFreePoint = floorElements[i].Position;
            while (obstaclesPositions.Contains(firstFreePoint))
            {
                i++;
                firstFreePoint = floorElements[i].Position;
            }

            return firstFreePoint;
        }

        private GameObject CreateObstacle(Vector3 at)
        {
            var obj = Object.Instantiate(_levelData.ObstaclePrefab, at, Quaternion.identity);
            obj.TryGetComponent<Obstacle>(out var obstacle);
            obstacle.Value = _levelData.ObstacleValue;
            obj.TryGetComponent<ObstacleHealth>(out var health);
            health.Max = _levelData.ObstacleHealth;
            obj.TryGetComponent<ObstacleVisual>(out var visual);
            visual.Construct(_data);
            obj.TryGetComponent<ObstacleDeath>(out var death);
            death.Construct(_data, _lootService);
            return obj;
        }
    }
}